最后更新时间:2019年7月5日
功能描述
路径引导,即导航功能,基于已有的规划路径实现导航,按照规划路径行进,辅以文字、图标引导,以及语音播报功能等。MapGIS Mobile SDK提供室内外一体化的路径引导接口,并且提供模拟导航,接入定位数据后可实现真实导航。此外,还能提供丰富的指引信息,比如当前道路的名称、当前导航动作、到达终点的距离等;提供路口放大图来准确指引路线;同时支持语音播报操作,提供更加友好、科学的智能化引导服务。
实现方法
路径引导(导航)功能实现的一般流程如下图所示:
1
构造路径引导对象MGSRouteGuide,并初始化路径引导对象,设置路线对象MGSRoute、路径分析对象MGSRouteAnalysis两个参数;
//构造路径引导对象 MGSRouteGuide *routeGuide=[[MGSRouteGuide alloc] init]; //利用导航路径、路径分析对象来初始化路径引导对象,这两个对象在路径分析阶段即可获取 [routeGuide init:route routeAnalysis:routeAnalysis];
2
(1)如果进行真实导航:可调用startGNSSNavi和stopGNSSNavi来开始、停止。
//开始真实导航 [routeGuide startGNSSNavi]; //停止真实导航 [routeGuide stopGNSSNavi]; //构建GNSS定位信息对象(需结合定位功能获取真实值) MGSGNSSLocInfo *gnssLocInfo=[[MGSGNSSLocInfo alloc] init]; gnssLocInfo.longitude=x; //经度 gnssLocInfo.latitude=y; //纬度 gnssLocInfo.speed=s; //速度 gnssLocInfo.hDop=p; //精度 gnssLocInfo.angle=a; //角度 //需根据实际定位不断向路径引导对象中推送定位信息 routeGuide.gnssLocInfo=gnssLocInfo;
代码说明:要实现真实导航,必须先实现实时定位功能,然后不断推送给MGSRouteGuide对象。实时定位功能实现,可采用iOS原生定位接口实现,或者利用第三方定位SDK实现(如高德定位SDK、百度定位SDK等)。不管采用哪一种方式进行定位,其接口中一般都会有监听函数,需在其中实例化MGSGNSSLocInfo对象,为其设置相关必要信息,然后设置给MGSRouteGuide对象。
(2)如果进行模拟导航:可调用startSimNavi、pauseOrResumeSimNavi:、stopSimNavi来开始、暂停/继续、停止模拟导航。
//开始模拟导航 [routeGuide startSimNavi]; //暂停模拟导航 [routeGuide pauseOrResumeSimNavi:YES]; //继续模拟导航,暂停了之后才能继续 [routeGuide pauseOrResumeSimNavi:NO]; //停止模拟导航 [routeGuide stopSimNavi]; //加速、减速:改变模拟导航参数 //创建模拟导航的参数信息(模拟速度:公里/小时;生成模拟点的频率:秒) MGSSimNaviOptions *simNaviOptions=[[MGSSimNaviOptions alloc] initWithSimNaviOptions:100.0f simNaviFrequency:1.0f]; //设置模拟导航的控制参数 [routeGuide setSimNaviOptions:simNaviOptions];
代码说明:模拟导航时需要传入模拟的运动速度,以及生成模拟点的频率,内部会根据这些参数模拟运动效果。
3
为路径引导对象设置NavigationDelegate导航相关状态监听器的代理协议,并实现其回调函数,在其中获取导航指引信息,可将其以多种形式展现,如文本控件、图像视图控件等,给用户以提示。
导航,即路径引导,是一个实时、动态的过程,随着用户移动设备位置的变化,导航功能提供的信息也随之变化,所以指引信息也需要动态地获取。SDK目前没有提供默认的导航引导界面,指引信息的展示工作需要开发人员自己编码实现。SDK具有导航状态代理,包括6个回调函数,每个函数提作用都不同,在其中提供了不同的指引信息。需要说明的是,这6个回调函数都是在子线程中执行的,如果获取信息后要操作UI界面,需要在主线程中执行。
//遵守代理协议 @interface OutdoorRouteGuide_ViewController ()<NavigationDelegate> //设置代理协议 [routeGuide setDelegate:self]; //导航指引信息监听 -(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{ //这是最重要、使用最多的一个回调函数,可获取诸多导航指引信息,例如当前道路名称、总里程剩余值、导航主动作等等,在下方会详细介绍各种方法如何获取、使用 } //真实导航状态变更回调监听 -(void)gnssNaviMode:(MGSGNSS_NAVI_MODE)gnssNaviMode statusEx:(int)statusEx{ switch (gnssNaviMode) { case GNSS_NAVI_MODE_START: NSLog(@"导航状态:导航开始"); break; case GNSS_NAVI_MODE_ARRIVE_MID: NSLog(@"导航状态:到达途径点"); NSLog(@"到达途经点索引值:%d",statusEx); break; case GNSS_NAVI_MODE_LEEWAY: NSLog(@"导航状态:偏离路径"); break; case GNSS_NAVI_MODE_STOP: NSLog(@"导航状态:导航停止"); break; case GNSS_NAVI_MODE_ARRIVE_DEST: NSLog(@"导航状态:到达终点"); break; default: break; } } //模拟导航状态变更回调监听 -(void)simNaviMode:(MGSSIM_NAVI_MODE)simNaviMode{ switch (simNaviMode) { case SIM_NAVI_MODE_START: NSLog(@"模拟导航状态:模拟导航开始"); break; case SIM_NAVI_MODE_PAUSED: NSLog(@"模拟导航状态:模拟导暂停中"); break; case SIM_NAVI_MODE_PROCESSING: NSLog(@"模拟导航状态:模拟导航进行中"); break; case SIM_NAVI_MODE_STOP: NSLog(@"模拟导航状态:模拟导航停止"); break; case SIM_NAVI_MODE_FINISH: NSLog(@"模拟导航状态:模拟导航完成"); break; default: break; } } //处理定位信息监听 -(void)gnssLocInfo:(MGSGNSSLocInfo *)gnssLocInfo matchLng:(double)matchLng matchLat:(double)matchLat matchFloor:(int)matchFloor matchAngle:(float)matchAngle{ //可获取:卫星状态、卫星数、道路匹配后的坐标、楼层、角度等信息 double lng=[gnssLocInfo longitude]; //原始经度 double lat=[gnssLocInfo latitude]; //原始纬度 NSLog(@"当前GNSS原始信息:经度:%f,纬度:%f",lng,lat); NSLog(@"道路匹配后的坐标经度值:%f",matchLng); NSLog(@"道路匹配后的坐标纬度值:%f",matchLat); NSLog(@"道路匹配后的楼层信息—室内导航专用信息:%d",matchFloor); NSLog(@"道路匹配后的角度值:%f",matchAngle); } //定位状态监听 -(void)gnssLocStatus:(MGSGNSS_LOC_STATUS)locStatus{ //获取定位状态:设备是否已连接、已定位等 switch (locStatus) { case GNSS_LOC_STATUS_SEARCHING: NSLog(@"定位状态:设备搜索中"); break; case GNSS_LOC_STATUS_FIXING: NSLog(@"定位状态:设备已连接,但未定位"); break; case GNSS_LOC_STATUS_FIXED: NSLog(@"定位状态:设备已连接并已定位"); break; case GNSS_LOC_STATUS_DISCONNECT: NSLog(@"定位状态:设备不可用,如移动端无GNSS模块,或者参数配置失败导致设备无法连接"); break; default: break; } } //播报指引信息监听 -(void)playNaviMessage:(NSString *)message gnssNavi:(BOOL)gnssNavi enPriority:(int)enPriority{ //获取导航指引语音信息,如:前方200米向左行驶等等 NSLog(@"播报指引信息:%@",message); //特别说明:MapGIS Mobile 10.3 for iOS SDK没有提供内置的语音播报功能,用户可借助第三方开发库实现语音播报 }
4
导航相关状态监听器的代理协议NavigationDelegate中有上述6个回调函数,其中最重要、最常用的函数是导航指引信息的函数routeNaviInfo:gnssNavi:,从此函数中可获取到导航路径信息MGSRouteNaviInfo对象,由此对象获取很多导航过程中非常重要的指引信息。下面将详细介绍信息如何获取与其展示方法:
普通导航指引信息:即界面上提示的当前道路名称、总路程剩余距离、当前行驶速度等信息,这些信息从RouteNaviInfo对象中获取;获取信息后进行界面展示,让用户知道当前行驶的进度与状态。
//导航指引信息回调函数 -(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{ NSString *currentRoadName=[routeNaviInfo curRoadName]; //当前道路名称 int currentRoadRemainDistance=[routeNaviInfo segRemainDis]; //当前路段剩余距离 NSString *nextRoadName=[routeNaviInfo nextRoadName]; //下一道路名称 int currentSpeed=[routeNaviInfo speed]; //当前行驶速度 int remainDistance=[routeNaviInfo remainDis]; //路程总剩余距离 int remainTime=[routeNaviInfo remainTime]; //路程总剩余时间 }
对于时间、距离信息,可以将上述接口返回的值进行处理,转换为合适单位的值,从而更加适合用户的查看体验。
//处理距离字符串:以合适的单位显示 -(NSString *)dealDistanceText:(int)distance{ NSString *strDistance; if (distance > 1000) { if ((distance % 1000) < 100 || (distance % 1000) == 0) { strDistance=[NSString stringWithFormat:@"%.0f公里",((float)distance/1000)]; } else { strDistance=[NSString stringWithFormat:@"%.1f公里",((float)distance/1000)]; } } else { strDistance=[NSString stringWithFormat:@"%d米",distance]; } return strDistance; } //处理时间字符串:以合适的单位显示 -(NSString *)dealTimeText:(int)time{ NSString *strTime; if (time < 60) { strTime=[NSString stringWithFormat:@"%d秒",time]; } else if (60 < time && time < 3600){ strTime=[NSString stringWithFormat:@"%.0f分钟",(float)(time/60)]; } else if (time > 3600){ strTime=[NSString stringWithFormat:@"%.1f小时",(float)(time/3600)]; } return strTime; }
上述这些文字信息,开发人员可以在界面上创建一些UILabel文本控件,然后在回调函数中动态修改控件的文本值,即可实现提示作用。需注意这些回调函数都是在子线程中执行的,如果要修改UI控件,需返回到主线程再做操作。
导航动作信息:即路径引导中的方向信息,告知用户该向什么方向前进,如左转弯、右转弯等。导航动作信息也是从MGSRouteNaviInfo对象中获取,得到short类型的16进制变量,不同的值对应不同的动作,具体见如下核心代码。
//导航指引信息回调函数 -(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{ //导航主动作 short naviAction=[routeNaviInfo naviAction]; //根据导航主动作信息获取对应的图像 UIImage *directionImage=nil; NSString *directionImageName; switch (action) { //直行 case 0x8: directionImageName=@"direction_line"; break; //左转 case 0x1: directionImageName=@"direction_zz"; break; //右转 case 0x2: directionImageName=@"direction_yz"; break; ······ default: break; } directionImage=[UIImage imageNamed:directionImageName]; }
代码说明:获取的导航主动作编码与主动作对应关系为:0x8(继续直行)、0x1(左转)0x2(右转)、0x9(靠左行驶)、0xA(靠右行驶)、0x7(左转调头)、0x0B(进入环岛)、0x0C(离开环岛)。室内专用导航主动作编码:0x10(沿楼梯向上)、0x11(沿楼梯向下)、0x12(沿电梯向上)、0x13(沿电梯向下)、0x14(进入建筑物)、0x15(离开建筑物)。
获取导航动作信息后,需要将此信息通过界面展示给用户,一般通过图片形式展现当前导航动作,能够非常直观地告知用户该往什么方向行驶。要实现此效果,通常在布局设计时添加一个UIImageView图像视图控件,在获取导航动作信息之后,根据对应关系为UIImageView动态设置图片。
运动目标的当前位置信息:导航过程中另外一个很重要的信息是行人或者车辆当前的位置,一般通过在地图上绘制图标MGSGraphicImage来表示。由于导航是一个动态变化的过程,运动物体的位置、前进的方向在导航过程中是不断变化的,因此需要动态变更其位置点与行进角度。
//导航指引信息回调函数 -(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{ //定位目标位置经推算后的位置信息 MGSDot currentPosition=[routeNaviInfo position]; //定位目标位置推算后的角度信息.与Y轴正方向的夹角,范围为[0,360],逆时针为正 float angle=[routeNaviInfo angle]; //地图的旋转角度,单位度,逆时针为正 float positionAngle=360-angle; /* 绘制图标,作为导航目标的位置。并需要动态修改定位图标的位置,动态地展示导航运动过程 说明:MGSGraphicImage只需创建一次,后续更新位置只需修改其位置点就行 */ if (_locationGraphicImage == nil) { _locationGraphicImage=[[MGSGraphicImage alloc] init]; [_locationGraphicImage setImage:[UIImage imageNamed:@"navigation"]]; [_locationGraphicImage setAnchorPoint:CGPointMake(0.5f, 0.5f)]; [_mapView.graphicsOverlay addGraphic:_locationGraphicImage]; } [_locationGraphicImage setPoint:currentPosition]; /* 动态修改地图的位置:将导航过程中当前位置设置为地图中心,并且旋转地图使得导航视角跟随车头,当然也可以根据需要不修改地图的位置、角度 */ MGSMapPosition *mapPosition=[[MGSMapPosition alloc] initWithCenter:currentPosition resolution:[_mapView resolution] rotateCenter:currentPosition rotateAngle:positionAngle slopeAngle:[_mapView slopeAngle]]; //地图位置(中心点,分辨率,旋转中心,旋转角,倾斜角) [_mapView updatePosition:mapPosition animated:YES]; //更新位置 [_mapView refresh]; }
运动目标的地图位置显示效果如下图所示,可以根据路径的方向来改变地图的旋转角度,使导航视角跟随车头,与行人或者车辆真实情况中的朝向一致,营造一定程度的沉浸感。
路口放大图:将路口的分支情况以图片的形式放大展示,特别针对于复杂情况的路口,如典型的环岛,通过路口放大图,可以更加清晰地指引用户该往哪个方向行进。路口放大图的显示,需要自定义一个UIImageView控件,通过SDK中的代码获取UIImage位图,然后赋给图像视图控件,将当前路口放大图展示在界面中。
//导航指引信息回调函数 -(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{ //路口放大图背景ID int crossBackID=[routeNaviInfo crossBackID]; //路口放大图箭头ID int crossArrowID=[routeNaviInfo crossArrowID]; //根据背景ID、箭头ID生成路口放大图 UIImage *crossImage=[self getCrossImageWithCrossBackID:crossBackID andCrossArrowID:crossArrowID]; //回到主线程 dispatch_async(dispatch_get_main_queue(), ^{ if (crossImage != nil) { [_ivCrossView setImage:crossImage]; //为UIImageView设置图片 } }); } //根据背景ID、箭头ID生成路口放大图 -(UIImage *)getCrossImageWithCrossBackID:(int)crossBackID andCrossArrowID:(int)crossArrowID{ //路口放大图是由路线对象和地图视图生成的 if (_route == nil || _mapView == nil) { return nil; } //如果剩余路段少于两条时不生成放大图 if (crossBackID < 0 || crossBackID > _route.stepCount-2) { return nil; } //根据ID获取当前路段、下一路段、下下路段 MGSDriveWalkSegment *driveWalkSegment=(MGSDriveWalkSegment *)[_route getStep:crossBackID]; MGSDriveWalkSegment *driveWalkSegmentNext=(MGSDriveWalkSegment *)[_route getStep:(crossBackID+1)]; MGSDriveWalkSegment *driveWalkSegmentNextNext=nil; //获取路段的节点数组、节点数目 MGSDot *curSegDots=[driveWalkSegment shapes]; long curSegDotsCount=[driveWalkSegment getPointCount]; MGSDot *nextSegDots=[driveWalkSegmentNext shapes]; long nextSegDotsCount=[driveWalkSegmentNext getPointCount]; //路口放大图imageview的高、宽 int width=_ivCrossView.frame.size.width; int height=_ivCrossView.frame.size.height; UIImage *image; //如果为环岛,处理方式不一样 if ([driveWalkSegmentNext.form rangeOfString:@"环岛"].location == NSNotFound) { //为非环岛,普通路口,普通处理 //利用MGSMapView根据传入的参数生成对应路口的放大图 image=[_mapView getCrossImageWithSeg1Pnts:curSegDots seg1PntCnt:(int)curSegDotsCount seg2Pnts:nextSegDots seg2PntCnt:(int)nextSegDotsCount seg3Pnts:nil seg3PntCnt:0 imageWidth:width imageHeigth:height]; } else { //环岛,需特殊处理 //如果背景ID大于减3之后的路径路段数,则不构建放大图 if (crossBackID > (_route.stepCount-3)) { return nil; } //获取当前路段的下下条路段,以及路段的点集、点个数 driveWalkSegmentNextNext=(MGSDriveWalkSegment *)[_route getStep:(crossBackID+2)]; MGSDot *nextNextSegDots=[driveWalkSegmentNextNext shapes]; long nextNextSegDotsCount=[driveWalkSegmentNextNext getPointCount]; //利用MGSMapView根据传入的参数生成对应路口的放大图 image=[_mapView getCrossImageWithSeg1Pnts:curSegDots seg1PntCnt:(int)curSegDotsCount seg2Pnts:nextSegDots seg2PntCnt:(int)nextSegDotsCount seg3Pnts:nextNextSegDots seg3PntCnt:(int)nextNextSegDotsCount imageWidth:width imageHeigth:height]; } return image; }
代码说明:路口放大图主要分为普通路口、环岛路口两种,对于环岛需要特殊处理。路口放大图的生成,需要使用MGSMapView地图控件的getCrossBitmap方法实现。例如,普通路口放大图效果如下图所示。